/**
 * BibSonomy-Model - Java- and JAXB-Model.
 *
 * Copyright (C) 2006 - 2016 Knowledge & Data Engineering Group,
 *                               University of Kassel, Germany
 *                               http://www.kde.cs.uni-kassel.de/
 *                           Data Mining and Information Retrieval Group,
 *                               University of Würzburg, Germany
 *                               http://www.is.informatik.uni-wuerzburg.de/en/dmir/
 *                           L3S Research Center,
 *                               Leibniz University Hannover, Germany
 *                               http://www.l3s.de/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.bibsonomy.model.util;

import static org.bibsonomy.util.ValidationUtils.present;

import java.net.URL;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import org.bibsonomy.common.enums.GroupID;
import org.bibsonomy.common.enums.ProfilePrivlevel;
import org.bibsonomy.common.enums.Role;
import org.bibsonomy.model.Group;
import org.bibsonomy.model.Tag;
import org.bibsonomy.model.User;
import org.bibsonomy.model.user.remote.RemoteUserId;
import org.bibsonomy.util.HashUtils;
import org.bibsonomy.util.StringUtils;

/**
 * @author Dominik Benz
 * @author Miranda Grahl
 */
public class UserUtils {
	
	/** the name of the dblp user */
	public static final String DBLP_USER_NAME = "dblp";
	
	/** the name of the genealogy user (import from dnb) */
	public static final String GENEALOGY_USER = "genealogie";
	
	/** a set of special users */
	public static final List<String> USER_NAMES_OF_SPECIAL_USERS = Arrays.asList(DBLP_USER_NAME, GENEALOGY_USER);
	
	private static final Map<String, List<Tag>> TAGS_OF_SPECIAL_USERS;
	
	static {
		TAGS_OF_SPECIAL_USERS = new HashMap<>();
		TAGS_OF_SPECIAL_USERS.put(DBLP_USER_NAME, createFrequentTags(DBLP_USER_NAME));
		TAGS_OF_SPECIAL_USERS.put(GENEALOGY_USER, createFrequentTags("dnb"));
	}

	private static List<Tag> createFrequentTags(final String... tagNames) {
		final List<Tag> tags = new ArrayList<Tag>();
		for (String tagName : tagNames) {
			tags.add(createFrequentTag(tagName));
		}
		return tags;
	}
	
	private static Tag createFrequentTag(final String tagName) {
		final Tag tag = new Tag();
		tag.setName(tagName);
		tag.setGlobalcount(1000000);
		tag.setUsercount(1000000);
		return tag;
	}
	
	/** the length of the password salt */
	private static final int SALT_LENGTH = 16;

	/**
	 * Validates the correctness of the email-address. This is done by 
	 * some simple tests, e.g., if the address contains whitespace, an '@'
	 * or a '.'.
	 * 
	 * @param email
	 * @return <code>true</code> if email is a valid email
	 */
	public static boolean isValidMailAddress (final String email) {
		return present(email) &&
				email.indexOf(' ') == -1 && // white space
				email.indexOf('@') != -1 && // no @
				email.length() <= 255 &&  // to long
				email.lastIndexOf(".") > email.lastIndexOf("@") && // no domain specified
				email.lastIndexOf("@") == email.indexOf("@") && // more than one @
				email.length() - email.lastIndexOf(".") > 2;
	}
	
	/**
	 * Checks the validity of the homepage. The homepage might either be NULL 
	 * or a http (or https) address.
	 * 
	 * @param homepage
	 * @return <code>true</code> iff homepage is valid
	 */
	public static boolean isValidHomePage(final URL homepage) {
		return !present(homepage) || "http".equals(homepage.getProtocol()) || "https".equals(homepage.getProtocol());
	}


	/**
	 * Checks, if the given user is the special DBLP user 
	 * (which has some special rights).
	 *  
	 * @param user
	 * @return <code>true</code>, if <code>user</code> is the DBLP user.
	 */
	public static boolean isDBLPUser(final User user) {
		return present(user) && isDBLPUser(user.getName());
	}


	/** Checks, if the given user name is the special DBLP user 
	 * (which has some special rights).
	 *  
	 * @param userName - the name of the user in question.
	 * @return <code>true</code>, if <code>user</code> is the DBLP user.
	 */
	public static boolean isDBLPUser(final String userName) {
		return DBLP_USER_NAME.equalsIgnoreCase(userName);
	}

	/**
	 * Generates an Api key with a MD5 message digest from a random number.
	 * 
	 * @return String Api key
	 */
	public static String generateApiKey() {
		return HashUtils.getMD5Hash(generateRandom());
	}

	private static byte[] generateRandom() {
		final byte[] randomBytes = new byte[32];
		try {
			new Random().nextBytes(randomBytes);
		} catch (final Exception e) {
			throw new RuntimeException(e);
		}
		return randomBytes;
	}

	/**
	 * Helper function to set a user's groups by a list of group IDs
	 * 
	 * @param user
	 * @param groupIDs
	 */
	public static void setGroupsByGroupIDs(final User user, final List<Integer> groupIDs) {
		for (final int groupID : groupIDs) {
			user.addGroup(new Group(groupID));
		}
	}
	
	/**
	 * Generates the ActivationCode for a specific user.
	 * 
	 * @param user
	 * @return activationCode
	 */
	public static String generateActivationCode(final User user) {
		final String prepareStatement = user.getName() + user.getIPAddress() + user.getApiKey() + new String(generateRandom());
		return HashUtils.getMD5Hash(prepareStatement.getBytes());
	}
	
	/**
	 * @return a random password (e.g. for openid and saml users)
	 */
	public static String generateRandomPassword() {
		final byte[] bytes = new byte[16];
		new Random().nextBytes(bytes);
		return HashUtils.getMD5Hash(bytes);
	}
	
	
	/**
	 * @param password the plaintext password
	 * @param salt the salt
	 * @return the correct hashed password for saving in the db
	 */
	public static String getPassword(final String password, final String salt) {
		return StringUtils.getMD5Hash(StringUtils.getMD5Hash(password) + salt);
	}
	
	/**
	 * @param user
	 * @param password
	 */
	public static void setupPassword(final User user, final String password) {
		final String passwordSalt = generateSalt();
		user.setPasswordSalt(passwordSalt);
		user.setPassword(getPassword(password, passwordSalt));
	}

	private static String generateSalt() {
		final byte[] bytes = new byte[SALT_LENGTH / 2];
		new SecureRandom().nextBytes(bytes);
		return HashUtils.toHexString(bytes);
	}

	/**
	 * Helper function to get a list of group IDs from the user's list of groups
	 * 
	 * @param user
	 * @return list of groupIDs extracted from the given user's list of groups
	 */
	public static List<Integer> getListOfGroupIDs(final User user) {
		final List<Integer> groupIDs = new ArrayList<Integer>();
		final List<Group> groups = getListOfGroups(user);
		for (final Group group : groups) {
			groupIDs.add(group.getGroupId());
		}
		return groupIDs;
	}

	/**
	 * Helper function to get a list of groups from the user's list of groups
	 * 
	 * @param user
	 * @return list of groups extracted from the given user's list of groups
	 */
	public static List<Group> getListOfGroups(final User user) {
		final List<Group> groups = new ArrayList<Group>();
		/*
		 * every user may see public posts
		 */ 
		groups.add(new Group(GroupID.PUBLIC));
		if (user == null) {
			return groups;
		}
		groups.addAll(user.getGroups());
		return groups;
	}	

	/**
	 * helper function to extract the usernames from a list of user objects into
	 * a set.
	 * 
	 * @param users
	 * @return
	 */
	public static Set<String> getHashSetOfUsernames(List<User> users) {
		if (!present(users)) {
			return null;
		}
		final Set<String> result = new HashSet<String>();
		for (final User u : users) {
			result.add(u.getName());
		}
		return result;
	}

	/**
	 * Check whether the user is a group by comparing his name with the names
	 * of all groups he belongs to. If a group exists with the user's name, the
	 * user is a group.
	 * 
	 * @param user
	 * @return boolean 
	 */
	public static boolean userIsGroup(final User user) {
		if (user == null) return false;

		final String userName = user.getName();
		final List<Group> groups = user.getGroups();

		/*
		 * iterate over groups and check whether the user name equals a group name
		 */
		if (groups != null) {
			for (final Group group: groups) {
				if (userName.equalsIgnoreCase(group.getName())) {
					return true;
				}
			}
		}

		return false;
	}
	
	
	/**
	 * Update a user:
	 * In the existingUser all fields, that are set in updatedUser will be overwritten
	 * Warning: UserSettings are not Updated!
	 * @param existingUser = the user before the update
	 * @param updatedUser = the user with updated fields
	 * 
	 */
	public static void updateUser(final User existingUser, final User updatedUser) {
		// FIXME if existingUser should copy all properties from the one bean to the
		// other we might want to come up with a more generic version of existingUser
		// code block - so if we add a field to the User bean we don't have to
		// remember adding it here
		// The problem with that idea is, that NOT ALL properties are updated (e.g. name, registrationDate)
		existingUser.setEmail(!present(updatedUser.getEmail()) 	? existingUser.getEmail() : updatedUser.getEmail());
		existingUser.setPassword(!present(updatedUser.getPassword()) ? existingUser.getPassword() : updatedUser.getPassword());
		existingUser.setPasswordSalt(!present(updatedUser.getPasswordSalt()) ? existingUser.getPasswordSalt() : updatedUser.getPasswordSalt());
		existingUser.setRealname(!present(updatedUser.getRealname()) ? existingUser.getRealname() : updatedUser.getRealname());
		existingUser.setHomepage(!present(updatedUser.getHomepage()) ? existingUser.getHomepage() : updatedUser.getHomepage());
		existingUser.setApiKey(!present(updatedUser.getApiKey()) ? existingUser.getApiKey()	: updatedUser.getApiKey());
		existingUser.setBirthday(!present(updatedUser.getBirthday()) ? existingUser.getBirthday() : updatedUser.getBirthday());
		existingUser.setGender(!present(updatedUser.getGender()) ? existingUser.getGender() : updatedUser.getGender());
		existingUser.setUseExternalPicture(updatedUser.isUseExternalPicture());
		existingUser.setHobbies(!present(updatedUser.getHobbies()) ? existingUser.getHobbies() : updatedUser.getHobbies());
		existingUser.setInterests(!present(updatedUser.getInterests()) ? existingUser.getInterests() : updatedUser.getInterests());
		existingUser.setIPAddress(!present(updatedUser.getIPAddress()) ? existingUser.getIPAddress() : updatedUser.getIPAddress());
		existingUser.setOpenURL(!present(updatedUser.getOpenURL()) 	? existingUser.getOpenURL() : updatedUser.getOpenURL());
		existingUser.setPlace(!present(updatedUser.getPlace()) ? existingUser.getPlace() : updatedUser.getPlace());
		existingUser.setProfession(!present(updatedUser.getProfession()) ? existingUser.getProfession()  : updatedUser.getProfession());
		existingUser.setInstitution(!present(updatedUser.getInstitution()) ? existingUser.getInstitution() : updatedUser.getInstitution());
		
		existingUser.setOpenID(!present(updatedUser.getOpenID()) ? existingUser.getOpenID() : updatedUser.getOpenID());
		existingUser.setLdapId(!present(updatedUser.getLdapId()) ? existingUser.getLdapId() : updatedUser.getLdapId());
		for (final RemoteUserId ruid : updatedUser.getRemoteUserIds()) {
			existingUser.setRemoteUserId(ruid);
		}
		
		existingUser.setSpammer(!present(updatedUser.getSpammer()) ? existingUser.getSpammer() : updatedUser.getSpammer());
		
		/*
		 * currently: never change any role to the default role NOBODY
		 */
		final Role updatedRole = updatedUser.getRole();
		existingUser.setRole((!present(updatedRole) || Role.NOBODY.equals(updatedRole)) ? existingUser.getRole() : updatedRole);

		existingUser.setUpdatedBy(!present(updatedUser.getUpdatedBy()) ? existingUser.getUpdatedBy() : updatedUser.getUpdatedBy());
		existingUser.setUpdatedAt(!present(updatedUser.getUpdatedAt()) ? existingUser.getUpdatedAt() : updatedUser.getUpdatedAt());

		existingUser.setReminderPassword(!present(updatedUser.getReminderPassword()) ? existingUser.getReminderPassword() : updatedUser.getReminderPassword());
		existingUser.setReminderPasswordRequestDate(!present(updatedUser.getReminderPasswordRequestDate()) 	? existingUser.getReminderPasswordRequestDate() : updatedUser.getReminderPasswordRequestDate());
	}
	
	/**
	 * @param user
	 * @return <code>true</code> iff the user exists in the system
	 */
	public static boolean isExistingUser(final User user) {
		return present(user) && !Role.DELETED.equals(user.getRole()) && present(user.getName());
	}
	
	/**
	 * This method returns a new groupuser {@link User} for the given group
	 * 
	 * @param groupName the name of the group
	 * @return the group user
	 */
	public static User buildGroupUser(final String groupName) {
		final User user = new User(groupName);
		user.setPassword(generateRandomPassword());
		user.setRealname(""); // XXX: realname can't be null (db schema)
		user.setEmail(""); // XXX: email can't be null (db schema)
		user.setRole(Role.GROUPUSER);
		// every info of the group is public; XXX: maybe we want to change it
		user.getSettings().setProfilePrivlevel(ProfilePrivlevel.PUBLIC);
		return user;
	}
	
	/**
	 * Return the user's name in a nice format to be used e.g. in emails. If the user has a non-empty real name, that will be returned, otherwise the username.
	 * @param user
	 * @param atPrefix - in cases where the username is returned the at-prefix can be prepended: e.g. @sdo vs. sdo
	 * @return
	 */
	public static String getNiceUserName(final User user, final boolean atPrefix) {
		if (present(user.getRealname())) {
			return user.getRealname();
		} 
		return (atPrefix ? "@" : "") + user.getName();
	
	}

	/**
	 * @param user
	 * @return true iff there the given user is a special user (usually those with too many posts to handle)
	 */
	public static boolean isSpecialUser(User user) {
		return present(user) && isSpecialUser(user.getName());
	}

	/**
	 * @param userName
	 * @return true iff there the given user is a special user (usually those with too many posts to handle)
	 */
	public static boolean isSpecialUser(String userName) {
		return present(userName) && USER_NAMES_OF_SPECIAL_USERS.contains(userName);
	}

	/**
	 * @param requestedUserName
	 * @return tags used as a replacement for the tag list of the given special user (querying for the real tag list would be too slow)
	 */
	public static List<Tag> getTagsOfSpecialUser(String requestedUserName) {
		if (requestedUserName == null) {
			return null;
		}
		return TAGS_OF_SPECIAL_USERS.get(requestedUserName);
	}
}